package okhttp3.curl;

import com.google.common.base.Joiner;
import io.airlift.airline.*;
import okhttp3.*;
import okhttp3.internal.Util;
import okhttp3.internal.http.StatusLine;
import okhttp3.internal.http2.Http2;
import okhttp3.internal.platform.Platform;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.LoggingEventListener;
import okio.BufferedSource;
import okio.Okio;
import okio.Sink;

import java.util.logging.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Properties;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;


import static java.util.concurrent.TimeUnit.SECONDS;

@Command(name = Main.NAME, description = "A curl for the next-generation web.")
public class Main extends HelpOption implements Runnable {
    static final String NAME = "okcurl";
    static final int DEFAULT_TIMEOUT = -1;
    private static Logger frameLogger;

    static Main fromArgs(String... args) {
        return SingleCommand.singleCommand(Main.class).parse(args);
    }

    public Main() {
    }

    public Main(String url) {
        this.url = url;
    }

    public static void main(String... args) {
        fromArgs(args).run();
    }

    private static String versionString() {
        try {
            Properties prop = new Properties();
            InputStream in = Main.class.getResourceAsStream("/okcurl_version.properties");
            if (null != in) {
                prop.load(in);
                in.close();
                return prop.getProperty("version") != null ? prop.getProperty("version") : "1.0";
            } else {
                return "1.0";
            }
        } catch (IOException e) {
            return "1.0";
//            throw new AssertionError("Could not load okcurl-version.properties.");
        }
    }

    private static String protocols() {
        return Joiner.on(", ").join(Protocol.values());
    }

    @Option(name = {"-X", "--request"}, description = "Specify request command to use")
    public String method;

    @Option(name = {"-d", "--data"}, description = "HTTP POST data")
    public String data;

    @Option(name = {"-H", "--header"}, description = "Custom header to pass to server")
    public List<String> headers;

    @Option(name = {"-A", "--user-agent"}, description = "User-Agent to send to server")
    public String userAgent = NAME + "/" + versionString();

    @Option(name = "--connect-timeout", description = "Maximum time allowed for connection (seconds)")
    public int connectTimeout = DEFAULT_TIMEOUT;

    @Option(name = "--read-timeout", description = "Maximum time allowed for reading data (seconds)")
    public int readTimeout = DEFAULT_TIMEOUT;

    @Option(
            name = "--call-timeout",
            description = "Maximum time allowed for the entire call (seconds)")
    public int callTimeout = DEFAULT_TIMEOUT;

    @Option(name = {"-L", "--location"}, description = "Follow redirects")
    public boolean followRedirects;

    @Option(name = {"-k", "--insecure"},
            description = "Allow connections to SSL sites without certs")
    public boolean allowInsecure;

    @Option(name = {"-i", "--include"}, description = "Include protocol headers in the output")
    public boolean showHeaders;

    @Option(name = "--frames", description = "Log HTTP/2 frames to STDERR")
    public boolean showHttp2Frames;

    @Option(name = {"-e", "--referer"}, description = "Referer URL")
    public String referer;

    @Option(name = {"-V", "--version"}, description = "Show version number and quit")
    public boolean version;

    @Option(
            name = {"-v", "--verbose"},
            description = "Makes " + NAME + " verbose during the operation")
    public boolean verbose;

    @Arguments(title = "url", description = "Remote resource URL")
    public String url;

    private OkHttpClient client;

    @Override
    public void run() {
        if (showHelpIfRequested()) {
            return;
        }
        if (version) {
            System.out.println(NAME + " " + versionString());
            System.out.println("Protocols: " + protocols());
            return;
        }

        if (showHttp2Frames) {
            enableHttp2FrameLogging();
        }

        client = createClient();
        Request request = createRequest();
        try {
            Response response = client.newCall(request).execute();
            if (showHeaders) {
                System.out.println(StatusLine.get(response));
                Headers headers = response.headers();
                for (int i = 0, size = headers.size(); i < size; i++) {
                    System.out.println(headers.name(i) + ": " + headers.value(i));
                }
                System.out.println();
            }

            // Stream the response to the System.out as it is returned from the server.
            Sink out = Okio.sink(System.out);
            BufferedSource source = response.body().source();
            while (!source.exhausted()) {
                out.write(source.getBuffer(), source.getBuffer().size());
                out.flush();
            }

            response.body().close();
        } catch (IOException e) {
            e.fillInStackTrace();
        } finally {
            close();
        }
    }

    private OkHttpClient createClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.followSslRedirects(followRedirects);
        if (connectTimeout != DEFAULT_TIMEOUT) {
            builder.connectTimeout(connectTimeout, SECONDS);
        }
        if (readTimeout != DEFAULT_TIMEOUT) {
            builder.readTimeout(readTimeout, SECONDS);
        }
        if (callTimeout != DEFAULT_TIMEOUT) {
            builder.callTimeout(callTimeout, SECONDS);
        }
        if (allowInsecure) {
            X509TrustManager trustManager = createInsecureTrustManager();
            SSLSocketFactory sslSocketFactory = createInsecureSslSocketFactory(trustManager);
            builder.sslSocketFactory(sslSocketFactory, trustManager);
            builder.hostnameVerifier(createInsecureHostnameVerifier());
        }
        if (verbose) {
            HttpLoggingInterceptor.Logger logger = System.out::println;
            builder.eventListenerFactory(new LoggingEventListener.Factory(logger));
        }
        return builder.build();
    }

    private String getRequestMethod() {
        if (method != null) {
            return method;
        }
        if (data != null) {
            return "POST";
        }
        return "GET";
    }

    private RequestBody getRequestBody() {
        if (data == null) {
            return null;
        }
        String bodyData = data;

        String mimeType = "application/x-www-form-urlencoded";
        if (headers != null) {
            for (String header : headers) {
                String[] parts = header.split(":", -1);
                if ("Content-Type".equalsIgnoreCase(parts[0])) {
                    mimeType = parts[1].trim();
                    headers.remove(header);
                    break;
                }
            }
        }

        return RequestBody.create(MediaType.parse(mimeType), bodyData);
    }

    Request createRequest() {
        Request.Builder request = new Request.Builder();

        request.url(url);
        request.method(getRequestMethod(), getRequestBody());

        if (headers != null) {
            for (String header : headers) {
                String[] parts = header.split(":", 2);
                request.header(parts[0], parts[1]);
            }
        }
        if (referer != null) {
            request.header("Referer", referer);
        }
        request.header("User-Agent", userAgent);

        return request.build();
    }

    private void close() {
        client.connectionPool().evictAll(); // Close any persistent connections.
    }

    private static X509TrustManager createInsecureTrustManager() {
        return new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };
    }

    private static SSLSocketFactory createInsecureSslSocketFactory(TrustManager trustManager) {
        try {
            SSLContext context = Platform.get().getSSLContext();
            context.init(null, new TrustManager[]{trustManager}, null);
            return context.getSocketFactory();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    private static HostnameVerifier createInsecureHostnameVerifier() {
        return (name, session) -> true;
    }

    private static void enableHttp2FrameLogging() {
        frameLogger = Logger.getLogger(Http2.class.getName());
        frameLogger.setLevel(Level.FINE);
        ConsoleHandler handler = new ConsoleHandler();
        handler.setLevel(Level.FINE);
        handler.setFormatter(new SimpleFormatter() {
            @Override
            public String format(LogRecord record) {
                return Util.format("%s%n", record.getMessage());
            }
        });
        frameLogger.addHandler(handler);
    }

    public Request get() {
        Request request = new Request
                .Builder()
                .url(url)
                .build();
        OkHttpClient okHttpClient = new OkHttpClient();

        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {


            }
        });
        return request;
    }
}

